/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package CCPVConflictArrowDrawer;

import SwingAnimation.AnimationAnimator;
import SwingAnimation.AnimationInterface;
import SwingAnimation.ColorPulser;
import SwingAnimation.ColorPulserInterface;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Point;
import java.awt.Polygon;
import java.util.HashMap;
import java.util.Timer;
import java.util.Vector;
import javax.swing.JPanel;

/**
 *
 * @author Heather
 */
public class CCPVArrowDrawer extends JPanel implements AnimationInterface
{

  private Image buffer;
  private Graphics bufferG;
  private static final int ARROW_HEAD_WIDTH = 20;
  private static final int ARROW_HEAD_LENGTH = 30;
  private Vector<CCPVArrow> arrows = new Vector<CCPVArrow>();
  private Timer animationTimer = new Timer();
  private Vector<ColorPulser> arrowPulsers = new Vector<ColorPulser>();
  // how often the animation will play in milliseconds
  public static final long animationInterval = 50;
  public boolean isAnimating = false;

  public CCPVArrowDrawer()
  {
    this.setOpaque(false);
  }

  public void addArrow(Point start, Point end, int thickness)
  {
    CCPVArrow arrow = generateArrow(start, end, thickness);
    arrows.add(arrow);
  }

  public void addArrow(Point start, Point end, int thickness, Color headColor, Color bodyColor, boolean isColorPulsing)
  {
    CCPVArrow arrow = generateArrow(start, end, thickness);
    arrows.add(arrow);
    arrow.headColor = headColor;
    arrow.bodyColor = bodyColor;
    arrow.isColorPulsing = isColorPulsing;
    arrow.setupAnimationSteps();
  }

  public void addArrow(Point start, Point end, int thickness, Color headColor, Color bodyColor, boolean isColorPulsing, int animationDuration)
  {
    CCPVArrow arrow = generateArrow(start, end, thickness);
    arrows.add(arrow);
    arrow.headColor = headColor;
    arrow.bodyColor = bodyColor;
    arrow.isColorPulsing = isColorPulsing;
    arrow.animationDuration = animationDuration;
    arrow.setupAnimationSteps();
  }

  // makes the smallest arrows the minThickess and the biggest the maxThickness. Everything between gets linearly scaled.
  public void scaleArrows(double minThickness, double maxThickness)
  {
    double currentMin = Integer.MAX_VALUE;
    double currentMax = Integer.MIN_VALUE;

    // find min and max
    for (CCPVArrow a : arrows)
    {
      if (a.thickness > currentMax)
      {
        currentMax = a.thickness;
      }

      if (a.thickness < currentMin)
      {
        currentMin = a.thickness;
      }
    }

    // at this point, we have 2 pts and we can form a line.  The "x" is the current min/max.  The "y" is the min or max thickness
    double slope = (maxThickness - minThickness) / (currentMax - currentMin);
    double yIntercept = maxThickness - (slope * currentMax);

    // now we have the equation for the line.  so we just convert all arrow thicknesses
    if (currentMin != currentMax) // if there are more than 2 coalitions....
    {
      Vector<CCPVArrow> newArrows = new Vector<CCPVArrow>();
      for (CCPVArrow a : arrows)
      {
        Point start = a.start;
        Point end = a.end;
        int thickness = (int) ((slope * a.thickness) + yIntercept);
        CCPVArrow arrow = generateArrow(start, end, thickness);
        arrow.headColor = a.headColor;
        arrow.bodyColor = a.bodyColor;
        arrow.isColorPulsing = a.isColorPulsing;
        arrow.animationDuration = a.animationDuration;
        arrow.setupAnimationSteps();
        newArrows.add(arrow);
      }
      arrows = newArrows;
    }
    else // if there are only 2 coalitions, than the max = min b/c there's only 1 arrow path
    {
      Vector<CCPVArrow> newArrows = new Vector<CCPVArrow>();
      for (CCPVArrow a : arrows)
      {
        Point start = a.start;
        Point end = a.end;
        int thickness = (int) maxThickness;
        CCPVArrow arrow = generateArrow(start, end, thickness);
        arrow.headColor = a.headColor;
        arrow.bodyColor = a.bodyColor;
        arrow.isColorPulsing = a.isColorPulsing;
        arrow.animationDuration = a.animationDuration;
        arrow.setupAnimationSteps();
        newArrows.add(arrow);
      }
      arrows = newArrows;
    }
  }

  public void clear()
  {
    arrows.clear();
    arrowPulsers.clear();
    stopAnimation();
  }

  private CCPVArrow generateArrow(Point start, Point end, int thickness)
  {
    CCPVArrow arrow = new CCPVArrow();
    arrow.thickness = thickness;
    // the hypotenuse of the triangle
    double hyp = thickness / 2.0;

    int startY = start.y;
    int endY = end.y;
    int startX = start.x;
    int endX = end.x;
    double slope = (double) (endY - startY) / (double) (endX - startX);
    double inverseSlope = -(1 / slope);

    Polygon body = new Polygon();

    int halfThickness = thickness / 2;
    double theta = Math.atan(Math.abs(inverseSlope));
    //double theta = Math.atan(inverseSlope);
    double xOffset = (halfThickness * Math.cos(theta));
    double yOffset = (halfThickness * Math.sin(theta));

    Point startCorner1;
    Point startCorner2;
    if (slope < 0)
    {
      body.addPoint(startX - (int) xOffset, startY - (int) yOffset);
      body.addPoint(endX - (int) xOffset, endY - (int) yOffset);
      body.addPoint(endX + (int) xOffset, endY + (int) yOffset);
      body.addPoint(startX + (int) xOffset, startY + (int) yOffset);

      startCorner1 = new Point(startX - (int) xOffset, startY - (int) yOffset);
      startCorner2 = new Point(startX + (int) xOffset, startY + (int) yOffset);
    }
    else
    {
      if (slope > 0)
      {
        body.addPoint(startX + (int) xOffset, startY - (int) yOffset);
        body.addPoint(endX + (int) xOffset, endY - (int) yOffset);
        body.addPoint(endX - (int) xOffset, endY + (int) yOffset);
        body.addPoint(startX - (int) xOffset, startY + (int) yOffset);

        startCorner1 = new Point(startX + (int) xOffset, startY - (int) yOffset);
        startCorner2 = new Point(startX - (int) xOffset, startY + (int) yOffset);
      }
      else
      {
        body.addPoint(startX, startY + halfThickness);
        body.addPoint(endX, endY + halfThickness);
        body.addPoint(endX, endY - halfThickness);
        body.addPoint(startX, startY - halfThickness);

        startCorner1 = new Point(startX, startY + halfThickness);
        startCorner2 = new Point(startX, startY - halfThickness);
      }
    }

    // head pts
    Point arrowHeadTip = getArrowHeadTip(start, end);
    Polygon head = new Polygon();
    head.addPoint(startCorner1.x, startCorner1.y);
    head.addPoint(startCorner2.x, startCorner2.y);
    head.addPoint(arrowHeadTip.x, arrowHeadTip.y);
    arrow.arrowHeadTip = arrowHeadTip;
    arrow.arrowBody = body;
    arrow.arrowHead = head;
    arrow.start = start;
    arrow.end = end;
    arrow.arrowLength = getDistance(start, end);
    arrow.parent = this;

    ColorPulser arrowPulser = new ColorPulser(arrow, Color.red);
    //arrowPulser.setToReallyFastBlinking();
    arrowPulsers.add(arrowPulser);
    arrowPulser.startAnimation();

    return arrow;
  }

  private Point getArrowHeadTip(Point start, Point end)
  {
    return getPointOnVectorIntegerPrecision(start, end, ARROW_HEAD_LENGTH);
  }

  // find a point along the given vector defined by the start and end pts.  This is a vector
  // and not a line b/c direction is important.  The returned point will always be along
  // the direction of the vector.
  private Point getPointOnVectorIntegerPrecision(Point start, Point end, double distance)
  {
    Point.Double doublePrecision = getPointOnVectorDoublePrecision(start, end, distance);
    return new Point((int) doublePrecision.x, (int) doublePrecision.y);
  }

  private Point.Double getPointOnVectorDoublePrecision(Point start, Point end, double distance)
  {
    double x = 0;
    double y = 0;
    double slope = (double) (end.y - start.y) / (double) (end.x - start.x);
    double theta = Math.atan(slope);

    if (start.x <= end.x) // if this is true, then the arrow pts left to right
    {
      if (slope >= 0)
      {
        x = start.x + (Math.cos(theta) * distance);
        y = start.y + (Math.sin(theta) * distance);
      }
      else
      {
        x = start.x + (Math.cos(theta) * distance);
        y = start.y + (Math.sin(theta) * distance);
      }
    }
    else // else the arrow pts right to left
    {
      if (slope >= 0)
      {
        x = start.x - (Math.cos(theta) * distance);
        y = start.y - (Math.sin(theta) * distance);
      }
      else
      {
        x = start.x - (Math.cos(theta) * distance);
        y = start.y - (Math.sin(theta) * distance);
      }
    }

    return new Point.Double(x, y);
  }

  public double getDistance(Point p1, Point p2)
  {
    double preRoot = Math.pow((p1.x - p2.x), 2) + Math.pow((p1.y - p2.y), 2);
    return Math.sqrt(preRoot);
  }

  public double getTheta(Point start, Point end)
  {
    double tanTheta = (end.y - start.y) / (end.x - start.x);
    return Math.atan(tanTheta);
  }

  public void setupAnimationSteps()
  {
    for (CCPVArrow a : arrows)
    {
      a.setupAnimationSteps();
    }
  }

  // returns true if the next animation is valid, false if not
  public boolean showNextAnimation()
  {
    if (!arrows.isEmpty())
    {
      for (CCPVArrow a : arrows)
      {
        a.showNextAnimation();
      }
      return true;
    }
    else
    {
      return false;
    }
  }

  public void startAnimation()
  {
    //setupAnimationSteps();
    animationTimer.scheduleAtFixedRate(new AnimationAnimator(this), 0, animationInterval);
    isAnimating = true;
  }

  public void stopAnimation()
  {
    //clear();
    animationTimer.cancel();
    animationTimer = new Timer();
    isAnimating = false;
  }

  // paints the animation
  public void paintAnimation()
  {
    repaint();
  }

  private void drawArrows(Graphics g)
  {
    for (CCPVArrow a : arrows)
    {
      //g.setColor(Color.black);
      //g.fillPolygon(a.arrowBody);
      if (isAnimating)
      {
        g.setColor(a.headColor);
        g.fillPolygon(a.arrowHead);
      }
    }
  }

  @Override
  public void paint(Graphics g)
  {
    drawArrows(g);
  }

  private class CCPVArrow implements ColorPulserInterface
  {
    // how long it will take an arrow head to scroll completely across the body

    public long animationDuration = 700;
    public CCPVArrowDrawer parent;
    public double arrowLength;
    public Point arrowHeadTip;
    public Point start;
    public Point end;
    public Polygon arrowHead;
    public Polygon arrowBody;
    public Color bodyColor;
    public Color headColor;
    public boolean isColorPulsing = false;
    public Vector<Polygon> animationSteps = new Vector<Polygon>();
    public int animationIndex = 0;
    public int thickness = 0;

    public void setupAnimationSteps()
    {
      animationSteps.clear();
      int numberOfSteps = (int) (animationDuration / animationInterval);
      // these are how much the arrow must move every timestep
      double xInterval;
      double yInterval;
      // this is the running total of where the arrow is
      double xRunning;
      double yRunning;

      // first we calculate the intervals
      double lengthInterval = arrowLength / numberOfSteps;
      Point.Double movePt = getPointOnVectorDoublePrecision(start, end, lengthInterval);
      xInterval = movePt.x - start.x;
      yInterval = movePt.y - start.y;

      // start w/ the first step
      Polygon firstStep = new Polygon();
      xRunning = arrowHeadTip.x + xInterval;
      yRunning = arrowHeadTip.y + yInterval;
      Point newHeadPt = new Point((int) (xRunning), (int) (yRunning));
      Polygon newHead = getArrowHeadMove(arrowHead, arrowHeadTip, newHeadPt);
      firstStep = newHead;
      animationSteps.add(firstStep);

      for (int i = 1; i < numberOfSteps; i++)
      {
        Polygon currentStep = new Polygon();
        double newXRunning = xRunning + xInterval;
        double newYRunning = yRunning + yInterval;
        xRunning = newXRunning;
        yRunning = newYRunning;
        newHeadPt = new Point((int) (xRunning), (int) (yRunning));
        newHead = getArrowHeadMove(arrowHead, arrowHeadTip, newHeadPt);
        currentStep = newHead;

        animationSteps.add(currentStep);
      }

      // the last step needs to be the original positions
      animationSteps.add(arrowHead);
      animationIndex = 0;
    }

    private Polygon getArrowHeadMove(Polygon oldHead, Point oldTip, Point newTip)
    {
      int xDiff = newTip.x - oldTip.x;
      int yDiff = newTip.y - oldTip.y;

      int[] oldXPts = oldHead.xpoints;
      int[] oldYPts = oldHead.ypoints;
      int[] newXPts = new int[oldXPts.length];
      int[] newYPts = new int[oldYPts.length];

      for (int i = 0; i < oldHead.npoints; i++)
      {
        newXPts[i] = oldXPts[i] + xDiff;
        newYPts[i] = oldYPts[i] + yDiff;
      }

      return new Polygon(newXPts, newYPts, oldHead.npoints);
    }

    public boolean showNextAnimation()
    {
      if (!animationSteps.isEmpty())
      {
        arrowHead = animationSteps.get(animationIndex);

        animationIndex++;
        if (animationIndex >= animationSteps.size())
        {
          animationIndex = 0;
        }

        return true;
      }
      else
      {
        return false;
      }
    }

    public void setColor(Color c)
    {
      headColor = c;
    }

    public void repaint()
    {
      parent.repaint();
    }
  }
}
